查看原文
其他

Python高级编程——描述符Descriptor超详细讲解(补充篇之底层原理实现)

草yang年华 机器学习与python集中营 2021-09-10

送你小心心记得关注我哦!!

进入正文

全文摘要

本文声明:前面的系列文章已经讲解了python属性的访问优先级、属性拦截器、Python的属性控制三剑客、Python的描述符协议,做了这么多的铺垫和原理讲解,但是还没有真正讲解python描述符的高级应用。看过前面文章的小伙伴一定有印象,Python描述符和Python装饰器息息相关,可以用来实现很多Python底层的相关设计,那具体到底是怎么实现的呢?这就是本文要解决的重点。本文依然是分为上、中、下、补充篇四个系列部分进行讲解,本文是第四篇——补充篇,介绍Python的描述符怎么实现一些python的高级功能,比如自定义实现@staticmethod、@classmethod、@property等功能。

上一篇文章中已经讲到,python描述符是可以实现大部分python类特性中的底层魔法,本文将以此为根基,实现几个基本的底层实现。

全文目录

 python描述符descriptor实现底层原理

                 01 实现底层@classmethod

                    02 实现底层@stasticmethod

                   03  实现底层@property

                    04  使用描述符实现属性类型约束


01

实现底层@classmethod


众所周知,在python语言的面向对象中,使用@classmethod修饰的方法是类方法,类方法可以通过实例名称访问,也可以通过类名称访问,我们平时在使用的过程中,都是这么实用的,从来没有想过这在语言底层到底怎么实现呢?

先看一个简单的例子:

class Person:
    def __init__(self):
        pass
    def study(cls):
        print('我会搞学习!')  

print(Person.study())

运行结果错误,显示TypeError: study() missing 1 required positional argument: 'cls'

我们都知道错误的原因是什么,我要定义类方法,我需要给study方法使用@classmethod装饰器,现在我自定义一个装饰器,我不使用原本的@classmethod,依然让它达到相同的效果。代码如下:

class NewDefine_classmethod:
    """
    使用“描述符”和“装饰器”结合起来,模拟@classmethod
    """

    def __init__(self, function):
        self.function = function

    def __get__(self, instance, owner):
        #对传进函数进行加工,最后返回该函数
        def wrapper(*args, **kwargs):   #使用不定参数是为了匹配需要修饰的函数参数
            print("给函数添加额外功能")
            self.function(owner, *args, **kwargs)
        return wrapper

class Person:
    name='我有姓名'
    def __init__(self):
        pass

    @NewDefine_classmethod
    def study_1(cls):
        print(f'我的名字是:{cls.name},我会搞学习!')

    @NewDefine_classmethod
    def study_2(cls,score):
        print(f'我的名字是:{cls.name},我会搞学习!,而且这次考试考了 {score} 分')

print(Person.study_1())
print(Person.study_2(99))

运行结果为:


给函数添加额外功能
我的名字是:我有姓名,我会搞学习!
None
给函数添加额外功能
我的名字是:我有姓名,我会搞学习!,而且这次考试考了 99 分
None


那到底是怎么运行的呢?结合前面的装饰其原理,我们可以分这样几步分析:

第一步:@NewDefine_classmethod本质上是一个“类装饰器”,从它的定义可知,它的定义为

class NewDefine_classmethod(function).我们发现,python系统定义的@classmethod其实它的定义也是一样的,如下,class classmethod(function) .怎么样?它们二者的定义是不是一样?

第二步:NewDefine_classmethod本质上又是一个描述符,因为在它的内部实现了__get__协议,由此可见,NewDefine_classmethod是“集装饰器-描述符”于一身的。

第三步:运行过程分析,因为study_1=NewDefine_classmethod(study_1),所以,study_1本质上是一个NewDefine_classmethod的对象,又因为NewDefine_classmethod本质上是实现了描述符的,所以,study_1本质上是一个定义在类中的描述符属性。

第四步:因为study_1本质上是一个定义在类中的描述符属性。所以在执行Person.study_1的时候,相当于是访问类的描述符属性,所以会进入到描述符的__get__方法。

现在是不是觉得原来python描述符还有这样神奇的使用呢?

注意:如果修饰的函数本身是具有返回值的,在__get__里面所定义的wrapper里面一定要返回,即return self.function(owner, *args, **kwargs)。

还有一个地方需要注意的是,因为这是自定义的底层实现,所以一些集成IDE可能会显示有语法错误,但是这没有关系,这正是python灵活多变的地方,运行并不会出现错误。


02

实现底层@stasticmethod 


staticmethod方法与classmethod方法的区别在于classmethod方法在使用需要传进一个类的引用作为参数。而staticmethod则不用。它们所遵循的原理是大致一样的,参见代码如下:

class NewDefine_staticmethod:
    """
    使用“描述符”和“装饰器”结合起来,模拟@classmethod
    """

    def __init__(self, function):
        self.function = function

    def __get__(self, instance, owner):
        #对传进函数进行加工,最后返回该函数
        def wrapper(*args, **kwargs):   #使用不定参数是为了匹配需要修饰的函数参数
            print("给函数添加额外功能")
            self.function(*args, **kwargs)
        return wrapper

class Person:
    name='我有姓名'
    def __init__(self):
        pass

    @NewDefine_staticmethod
    def study_1(math,english):
        print(f'我数学考了 {math} 分,英语考了 {english} 分,我会搞学习!')

    @NewDefine_staticmethod
    def study_2(history,science):
        print(f'我历史考了 {history} 分,科学考了 {science} 分,我会搞学习!')

print(Person.study_1(99,98))
print(Person.study_2(88,89))


运行结果为:


给函数添加额外功能

我数学考了 99 分,英语考了 98 分,我会搞学习!

None

给函数添加额外功能

我历史考了 88 分,科学考了 89 分,我会搞学习!

None


整个函数的执行原理与上面所实现的是一样的,但是有一些小的细节需要注意。

类方法classmethod必须第一个参数是cls,这个实际上就是判断所属的那个类,因此在__get__里面的function在调用的时候,第一个参数需要传递为owner,因为所属的“类cls等价于Person等价于owner”,但是因为静态方法不需要任何参数cls或者是self都不需要,因此在__get__实现的时候不能再传递owner参数,否则会显示参数错误。


03

实现底层@property 


在使用描述符实现property的时候和前面稍有所区别,后面会进行分析,代码如下:

class NewDefine_property:
    """
    使用“描述符”和“装饰器”结合起来,模拟@classmethod
    """

    def __init__(self, function):
        self.function = function

    def __get__(self, instance, owner):
        print("给函数添加额外功能")
        return self.function(instance)

class Person:
    name='我有姓名'
    def __init__(self):
        self.__study=100

    @NewDefine_property
    def study_1(self):  #使用property装饰的函数一般不要用“参数”,因为它的主要功能是对属性的封装
        return self.__study

p=Person()
print(p.study_1)


运行结果为:


给函数添加额外功能

100


基本思想和前面分析的还是一样的,但是有几个地方有所区别,需要注意:

第一:@property的目的是封装一个方法,是这个方法可以被当做属性访问

第二:调用的方式与前面有所不同,__get__里面不能再定义wrapper了,否则不会调用wrapper。得不到想要的结果,为什么呢?

因为调用的方式不一样,根据前面的分析,study_1的本质是描述符属性,但是前面的调用均是使用的

Person.study_1()或者是p.study_1()的形式,还是当成方法去使用的。但是此处不一样了,直接就是当成属性去使用,

p.study_1 ,不再是方法调用,因此wrapper函数得不到调用。所以__get__方法得到了进一步简化。


04

使用描述符实现属性类型约束


众所周知,python是一门动态语言,属性的赋值可以是任意的,不像是静态语言那样,而我们前面讲过,描述符最核心的功能其实就是“属性代理”,即所谓的对属性进行加工改造,进行相关的约束。所以,如果想要对某一个属性的类型进行某种约束,描述符便可以很好地完成。实现的代码如下:

class Constraint_Property(object):
    def __init__(self,var_name,var_type,var_default_value=None):
        '''
        var_name:变量名称
        var_type:变量所要约束的类型,比如int、str、float等等
        var_default_value:变量的初始默认值
        '''

        self.name=var_name
        self.type=var_type
        self.default=var_type() if var_default_value is None else var_default_value
        #三元运算,如果使用了默认值就使用默认值,否则就是用某个类型的默认值,如int()、str()、float()

    def __get__(self,instance,owner):
        if self.default==None:
            return self.type()
        else:
            return self.default

    def __set__(self,instance,value):
        if not isinstance(value,self.type):
            raise TypeError("属性的值必须是: %s 类型",self.type)
        self.default=value

    def __delete__(self,instance):
        raise AttributeError("不能删除该属性")

class Student(object):
    name=Constraint_Property("name",str,"张三")  #str虽然是类名,也是可以作为参数的,因为一切皆对象
    age=Constraint_Property("age",int)           #int虽然是类名,也是可以作为参数的,因为一切皆对象

stu=Student()
print(stu.name)
print(stu.age)
stu.name="李四"
print(stu.name)
stu.age=25
print(stu.age)
print('===================================')
stu.name=100.0  #赋一个实数值


运行结果为:


张三

0

李四

25

===================================

Traceback (most recent call last):TypeError: ('属性的值必须是: %s 类型', <class 'str'>)


从上面的实例可以看出,那么属性被限定为str,age的属性被限定为int。


小伙伴们,看了上面的这几个例子,是不是被python描述符的强大功能所折服呢?事实上python描述符的各种功能是非常强大的,这里就不一一实现了,有兴趣的小伙伴可以自己不断尝试哦!


推 荐 阅 读

Python高级编程——描述符Descriptor超详细讲解(下篇之描述符三剑客)

Python高级编程——描述符Descriptor超详细讲解(中篇之属性控制)

Python高级编程——装饰器Decorator超详细讲解(补充篇——嵌套装饰器)



2018/12/13

Thursday

小伙伴们,如果你耐心的看完了关于python描述符的系列文章(补充篇),是不是有所收获呢?后面小编还会更新一些系列文章,包括python的设计模式、深度神经网络的原理讲解、数据结构的Python实现,想要每天不断学习的小伙伴都可以关注一下哦,认真看完本系列文章,我相信一定会大有收获的。欢迎小伙伴们继续关注和支持。如果你有需要,就添加我的公众号哦,里面分享有海量资源,包含各类数据、教程等,后面会有更多面经、资料、数据集等各类干货等着大家哦,重要的是全都是免费、无套路分享,有兴趣的小伙伴请持续关注!











: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存